﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UMLSynthesizer.LangAnalyzer.Elements;
using UMLSynthesizer.LangAnalyzer.ExtensionMethods;
using UMLSynthesizer.LangAnalyzer.Statements;

namespace UMLSynthesizer.LangAnalyzer
{
    public abstract class LanguageAnalyzer
    {
        #region Declarations

        public enum Metrics
        {
            ACD,
            CC,
            CCC,
            CCD,
            CD,
            MCC,
            NCCD
        }

        #endregion

        #region Fields

        public static readonly ElementKinds[] ValidElementsForCD = new ElementKinds[]
        {
            ElementKinds.Class,
            ElementKinds.Interface,
            ElementKinds.Struct
        };

        #endregion

        #region Public Interface

        /// <summary>
        /// Get a project reference
        /// </summary>
        /// <param name="sourceFiles">The source code to analyze</param>
        /// <returns>The project</returns>
        public abstract IProjectElement GetProject(IEnumerable<string> sourceFiles,
            IEnumerable<string> additionalAssemblyReferences = null);

        /// <summary>
        /// Calculate the Component Dependency metric
        /// </summary>
        /// <param name="element">The element to analyze</param>
        /// <returns>Component Dependency metric</returns>
        public static int CalculateCD(IContainerOfElementsBase element)
        {
            if (element == null)
                throw new ArgumentNullException("element can't be null");
            if (!ValidElementsForCD.Contains(element.Kind))
                return 0;

            double value;
            if (!element.MetricDictionary.ContainsKey(Metrics.CD))
                element.MetricDictionary.Add(Metrics.CD,
                    (from type in element.GetDependenceTypes()
                     where type.Kind != ElementKinds.ExternalType //Exclude external types
                     where type.Kind != ElementKinds.ArrayType //Exclude array types
                     where element.GetContainedElement(type.Identifier) == null //don't count elements contained in this class
                     select type).Distinct().Count());
            element.MetricDictionary.TryGetValue(Metrics.CD, out value);
            return (int)value;
        }

        /// <summary>
        /// Calculate the Comulative Component Dependency metric
        /// </summary>
        /// <param name="project">The container to analyze</param>
        /// <returns>Comulative Component Dependency metric</returns>
        public static int CalculateCCD(IContainerOfElementsBase container)
        {
            if (container == null)
                throw new ArgumentNullException("container can't be null");

            double value;
            if (!container.MetricDictionary.ContainsKey(Metrics.CCD))
                container.MetricDictionary.Add(Metrics.CCD,
                    (from element in container.GetDescendantElements()
                     where ValidElementsForCD.Contains(element.Kind)
                     select CalculateCD(element as IContainerOfElementsBase)).Sum());
            container.MetricDictionary.TryGetValue(Metrics.CCD, out value);
            return (int)value;
        }

        /// <summary>
        /// Calculate the Average Component Dependency metric
        /// </summary>
        /// <param name="project">The container to analyze</param>
        /// <returns>Average Component Dependency metric</returns>
        public static double CalculateACD(IContainerOfElementsBase container)
        {
            if (container == null)
                throw new ArgumentNullException("container can't be null");

            double value;
            if (!container.MetricDictionary.ContainsKey(Metrics.ACD))
                container.MetricDictionary.Add(Metrics.ACD,
                    (double)CalculateCCD(container) / CountElementsForCD(container));
            container.MetricDictionary.TryGetValue(Metrics.ACD, out value);
            return value;
        }

        /// <summary>
        /// Calculate the Normalized Cumulative Component Dependency metric
        /// </summary>
        /// <param name="project">The container to analyze</param>
        /// <returns>Normalized Cumulative Component Dependency metric</returns>
        public static double CalculateNCCD(IContainerOfElementsBase container)
        {
            if (container == null)
                throw new ArgumentNullException("container can't be null");

            double value;
            if (!container.MetricDictionary.ContainsKey(Metrics.NCCD))
            {
                int n = CountElementsForCD(container);
                container.MetricDictionary.Add(Metrics.NCCD,
                    CalculateCCD(container) / ((n + 1) * (Math.Log(n + 1, 2) - 1) + 1));
            }
            container.MetricDictionary.TryGetValue(Metrics.NCCD, out value);
            return value;
        }

        /// <summary>
        /// Calculate the Cyclomatic Complexity metric
        /// </summary>
        /// <param name="method">The method to analyze</param>
        /// <returns>Cyclomatic Complexity metric</returns>
        public static int CalculateCC(IMethodElement method)
        {
            if (method == null)
                throw new ArgumentNullException("method can't be null");

            double value;
            if (!method.MetricDictionary.ContainsKey(Metrics.CC))
                method.MetricDictionary.Add(Metrics.CC,
                    CountDecisionalStatements(method.Body) - CountExitPoints(method.Body, true) + 2);
            method.MetricDictionary.TryGetValue(Metrics.CC, out value);
            return (int)value;
        }

        /// <summary>
        /// Calculate the Max Cyclomatic Complexity metric
        /// </summary>
        /// <param name="method">The container to analyze</param>
        /// <returns>Max Cyclomatic Complexity metric</returns>
        public static int CalculateMCC(IContainerOfElementsBase classElement)
        {
            if (classElement == null)
                throw new ArgumentNullException("class can't be null");

            double value;
            if (!classElement.MetricDictionary.ContainsKey(Metrics.MCC))
                classElement.MetricDictionary.Add(Metrics.MCC,
                    (from method in classElement.ContainedMethods
                     select CalculateCC(method)).Max());
            classElement.MetricDictionary.TryGetValue(Metrics.MCC, out value);
            return (int)value;
        }

        /// <summary>
        /// Calculate the Comulative Cyclomatic Complexity metric
        /// </summary>
        /// <param name="method">The container to analyze</param>
        /// <returns>Comulative Cyclomatic Complexity metric</returns>
        public static int CalculateCCC(IContainerOfElementsBase classElement)
        {
            if (classElement == null)
                throw new ArgumentNullException("class can't be null");

            double value;
            if (!classElement.MetricDictionary.ContainsKey(Metrics.CCC))
                classElement.MetricDictionary.Add(Metrics.CCC,
                    (from method in classElement.ContainedMethods
                     select CalculateCC(method)).Sum());
            classElement.MetricDictionary.TryGetValue(Metrics.CCC, out value);
            return (int)value;
        }

        /// <summary>
        /// Calculate a metric by parameter. Throw an exception if the element is invalid for the selected metric
        /// </summary>
        /// <param name="element">The element to be analyzed</param>
        /// <param name="metric">the metric to be calculated</param>
        /// <returns>The metric value</returns>
        public static double CalculateMetric(IElementBase element, LanguageAnalyzer.Metrics metric)
        {
            switch (metric)
            {
                case Metrics.ACD:
                    if (element as IContainerOfElementsBase == null)
                        throw new ArgumentException("The element type isn't compatible with the selected metric");
                    return CalculateACD(element as IContainerOfElementsBase);
                case Metrics.CC:
                    if (element as IMethodElement == null)
                        throw new ArgumentException("The element type isn't compatible with the selected metric");
                    return CalculateCC(element as IMethodElement);
                case Metrics.CCC:
                    if (element as IContainerOfElementsBase == null)
                        throw new ArgumentException("The element type isn't compatible with the selected metric");
                    return CalculateCCC(element as IContainerOfElementsBase);
                case Metrics.CCD:
                    if (element as IContainerOfElementsBase == null)
                        throw new ArgumentException("The element type isn't compatible with the selected metric");
                    return CalculateCCD(element as IContainerOfElementsBase);
                case Metrics.CD:
                    if (element as IContainerOfElementsBase == null)
                        throw new ArgumentException("The element type isn't compatible with the selected metric");
                    return CalculateCD(element as IContainerOfElementsBase);
                case Metrics.MCC:
                    if (element as IContainerOfElementsBase == null)
                        throw new ArgumentException("The element type isn't compatible with the selected metric");
                    return CalculateMCC(element as IContainerOfElementsBase);
                case Metrics.NCCD:
                    if (element as IContainerOfElementsBase == null)
                        throw new ArgumentException("The element type isn't compatible with the selected metric");
                    return CalculateNCCD(element as IContainerOfElementsBase);
                default:
                    return 0;
            }
        }

        #endregion

        #region Private helpers

        /// <summary>
        /// Count how many statements with decisional functions there are into the block
        /// </summary>
        /// <param name="blockStatement">The block to analyze</param>
        /// <returns>The number os statements</returns>
        static int CountDecisionalStatements(IBlockStatementBase blockStatement)
        {
            if (blockStatement == null)
                return 0;

            int counter = 0;
            bool ignoreFollowingNextcase = false;
            foreach (var statement in blockStatement.ContainedStatements)
            {
                // the Case statement is a special case
                if (statement.Kind == StatementKinds.Case)
                {
                    if (!ignoreFollowingNextcase)
                    {
                        counter++; //this is a new case block
                        ignoreFollowingNextcase = true;
                    }
                }
                else
                {
                    switch (statement.Kind)
                    {
                        case StatementKinds.Catch:
                        case StatementKinds.Do:
                        case StatementKinds.For:
                        case StatementKinds.Foreach:
                        case StatementKinds.If:
                        case StatementKinds.While:
                            counter++;
                            break;
                    }

                    if (statement as IBlockStatementBase != null)
                        counter += CountDecisionalStatements(statement as IBlockStatementBase);
                    if (statement as IIfStatement != null)
                        counter += CountDecisionalStatements((statement as IIfStatement).Else);
                    if (statement as ITryStatement != null)
                    {
                        foreach (var catchBlock in (statement as ITryStatement).Catches)
                            counter += CountDecisionalStatements(catchBlock);
                        counter += CountDecisionalStatements((statement as ITryStatement).Finally);
                    }
                    ignoreFollowingNextcase = false;
                }
            }

            return counter;
        }

        /// <summary>
        /// Return the number of elements with which you can calc the Component Dependency metric
        /// </summary>
        /// <param name="project">The container to analyze</param>
        /// <returns>The number of valid elements</returns>
        static int CountElementsForCD(IContainerOfElementsBase container)
        {
            return (from element in container.GetDescendantElements()
                    where ValidElementsForCD.Contains(element.Kind)
                    select element).Count();
        }

        /// <summary>
        /// Count how many explicit and implicit exit points there are into the block
        /// </summary>
        /// <param name="blockStatement">The block to analyze</param>
        /// <returns>The number os exit points</returns>
        static int CountExitPoints(IBlockStatementBase blockStatement, bool isMethodBodyBlock)
        {
            if (blockStatement == null)
                return 0;

            int counter = 0;
            bool foundReturn = false;
            foreach (var statement in blockStatement.ContainedStatements)
            {
                //if I've found a return and this isn't a switch block, this code is not executable.
                if (foundReturn && statement.Kind != StatementKinds.Case)
                    continue;

                foundReturn = false;
                if (statement.Kind == StatementKinds.Return)
                {
                    counter++;
                    foundReturn = true;
                }

                if (statement as IBlockStatementBase != null)
                    counter += CountExitPoints(statement as IBlockStatementBase, false);
                if (statement as IIfStatement != null)
                    counter += CountExitPoints((statement as IIfStatement).Else, false);
                if (statement as ITryStatement != null)
                {
                    foreach (var catchBlock in (statement as ITryStatement).Catches)
                        counter += CountExitPoints(catchBlock, false);
                    counter += CountExitPoints((statement as ITryStatement).Finally, false);
                }
            }

            //if there is an implicit exit point
            if (!foundReturn && isMethodBodyBlock)
                counter++;

            return counter;
        }

        #endregion
    }
}
